/*
 * Copyright (c) 2024 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { Expression, PropertyAccessExpression, SourceFile, SyntaxKind } from 'ts-morph';
import { IConstantResolver } from '../interface/IConstantResolver';
import { PluginFileUtil, PluginStore, TsAstUtil } from '../../../framework';
import { FilePathConstants } from '../../constants';
import { ImportAnalyzer } from './ImportAnalyzer';
import { ErrorCode, PluginError } from '../../error/PluginError';

export class ConstantResolver implements IConstantResolver {
  private importMapCache: Map<string, Map<string, string[]>> = new Map();
  private readonly modulePath: string;
  constructor(modulePath: string) {
    this.modulePath = modulePath;
  }

  /**
   * Parse constant value
   * @param value Value object to resolve
   * @param sourceFile Source file
   * @param filePath File path
   * @returns Parsed value
   */
  resolveConstant(value: any, sourceFile: SourceFile, filePath: string): any {
    if (value.type === 'constant') {
      // Constant type value
      return this.parseConstantValue(TsAstUtil.getSourceFile(value.variableFilePath), value.variableName);
    } else if (value.type === 'object') {
      // Static variable type value
      return this.parseConstantValue(
        TsAstUtil.getSourceFile(value.variableFilePath),
        value.variableName,
        value.propertyName
      );
    } else if (value.type === 'array') {
      // Array type value
      return value.value.map((item: any) => {
        return this.resolveConstant(item, sourceFile, filePath);
      });
    } else {
      // Normal type value
      return value;
    }
  }

  /**
   * Parse property value
   * @param value Expression node
   * @param sourceFile Source file
   * @param filePath File path
   * @returns Parsed property value
   */
  resolvePropertyValue(value: Expression, sourceFile: SourceFile, filePath: string): any {
    switch (value.getKind()) {
      case SyntaxKind.Identifier:
        if (value.getText() === 'undefined') {
          throw PluginError.create(ErrorCode.INVALID_STRING_VALUE, '', filePath);
        }
        // Constant value
        return {
          type: 'constant',
          variableName: value.getText(),
          variableFilePath: this.getVariableFilePath(value.getText(), sourceFile, filePath),
        };
      case SyntaxKind.PropertyAccessExpression:
        // Static variable value
        return {
          type: 'object',
          variableName: (value as PropertyAccessExpression).getExpression().getText(),
          propertyName: (value as PropertyAccessExpression)?.getName(),
          variableFilePath: this.getVariableFilePath(
            (value as PropertyAccessExpression)?.getExpression().getText(),
            sourceFile,
            filePath
          ),
        };
      case SyntaxKind.ArrayLiteralExpression:
        // Array value
        return {
          type: 'array',
          value: value
            .asKind(SyntaxKind.ArrayLiteralExpression)
          ?.getElements()
            .map((item) => this.resolvePropertyValue(item, sourceFile, filePath)),
        };
      default:
      // Parse primitive value
        return this.parsePrimitiveValue(value);
    }
  }

  /**
   * Get the path of the variable
   * @param variableName Variable name
   * @param sourceFile Source file
   * @param filePath File path
   * @returns File path of the variable
   */
  getVariableFilePath(variableName: string, sourceFile: SourceFile, filePath: string): string {
    let resultPath: string = '';

    // Check local variables and classes
    const classesNames = sourceFile.getClasses().map((classes) => classes.getName()!);
    const variableNames = sourceFile
      .getVariableDeclarations()
      .map((variableDeclaration) => variableDeclaration.getName());

    if (classesNames.includes(variableName) || variableNames.includes(variableName)) {
      return filePath;
    }

    // Get import map
    let importMap: Map<string, string[]>;
    if (this.importMapCache.has(filePath)) {
      importMap = this.importMapCache.get(filePath)!;
    } else {
      importMap = ImportAnalyzer.analyzeImports(sourceFile);
      this.importMapCache.set(filePath, importMap);
    }

    // Find in an import map
    for (const [importPath, importNames] of importMap.entries()) {
      if (importNames.includes(variableName)) {
        const currentDir = PluginFileUtil.pathResolve(filePath, FilePathConstants.PARENT_DELIMITER);
        const tempFilePath = PluginFileUtil.pathResolve(currentDir, importPath + FilePathConstants.ETS_SUFFIX);

        if (PluginFileUtil.exist(tempFilePath)) {
          resultPath = tempFilePath;
          break;
        }
      }
    }

    // If not found in an import map, try to find in other modules
    if (!resultPath) {
      for (const [importPath, importNames] of importMap.entries()) {
        if (importNames.includes(variableName)) {
          resultPath = this.getOtherModuleVariableFilePath(importPath, variableName, this.modulePath);
          break;
        }
      }
    }

    return resultPath;
  }

  /**
   * Get the path of the variable in another module
   * @param moduleName Module name
   * @param variableName Variable name
   * @param modulePath
   * @returns File path of the variable
   */
  getOtherModuleVariableFilePath(moduleName: string, variableName: string, modulePath: string): string {
    let moduleFilePath = PluginFileUtil.pathResolve(
      modulePath,
      FilePathConstants.OH_MODULE_PATH,
      moduleName,
      FilePathConstants.DEFAULT_SCAN_DIR
    );

    if (!PluginFileUtil.exist(moduleFilePath)) {
      // The module doesn't exist, need to search the project directory
      moduleFilePath = PluginFileUtil.pathResolve(
        PluginStore.getInstance().get('projectFilePath')!,
        FilePathConstants.OH_MODULE_PATH,
        moduleName,
        FilePathConstants.DEFAULT_SCAN_DIR
      );
    }

    const variableCache = PluginStore.getInstance().get<Map<string, Map<string, string[]>>>('variableCache');
    if (!variableCache) {
      PluginStore.getInstance().set('variableCache', new Map());
    }

    let variableMap;
    if (variableCache?.has(moduleName)) {
      variableMap = variableCache.get(moduleName)!;
    } else {
      variableMap = this.parseCrossModuleVariable(moduleFilePath);
      variableCache?.set(moduleName, variableMap);
    }

    for (let [key, value] of variableMap) {
      if (value.includes(variableName)) {
        return key;
      }
    }

    throw PluginError.create(ErrorCode.UNKNOWN_VARIABLE, '', variableName);
  }

  /**
   * Parse primitive value
   * @param value Expression node
   * @returns Parsed primitive value
   */
  private parsePrimitiveValue(value: Expression): any {
    let propertyValue;
    switch (value.getKind()) {
      case SyntaxKind.StringLiteral:
        propertyValue = value.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
        break;
      case SyntaxKind.NumericLiteral:
        propertyValue = value.asKind(SyntaxKind.NumericLiteral)?.getLiteralValue();
        break;
      case SyntaxKind.TrueKeyword:
        propertyValue = true;
        break;
      case SyntaxKind.FalseKeyword:
        propertyValue = false;
        break;
      case SyntaxKind.ArrayLiteralExpression:
        propertyValue = value
          .asKind(SyntaxKind.ArrayLiteralExpression)
        ?.getElements()
          .map((item) => item.asKind(SyntaxKind.StringLiteral)?.getLiteralValue());
        break;
    }
    return propertyValue;
  }

  /**
   * Parse constant value
   * @param sourceFile Resource file
   * @param variableName Variable name/class name
   * @param propertyName Property name
   * @return string Constant value
   */
  private parseConstantValue(sourceFile: SourceFile, variableName: string, propertyName?: string): string {
    let result: string | Expression;
    // Determine whether it's a constant or an object based on the presence of propertyName
    if (propertyName) {
      // object
      let classInstance = sourceFile.getClasses().find((classes) => {
        return classes.getName() === variableName;
      });
      if (!classInstance) {
        throw PluginError.create(ErrorCode.UNKNOWN_CLASS, '', variableName);
      }
      let property = classInstance.getProperties().find((properties) => {
        return properties.getName() === propertyName;
      });
      if (!property) {
        throw PluginError.create(ErrorCode.UNKNOWN_PROPERTY, '', propertyName);
      }
      result = property.getInitializer()!;
    } else {
      // constant
      let constant = sourceFile.getVariableDeclarations().find((declaration) => {
        return declaration.getName() === variableName;
      });
      if (!constant) {
        throw PluginError.create(ErrorCode.UNKNOWN_CONSTANT, '', variableName);
      }
      result = constant.getInitializer()!;
    }
    // Check constant type
    if (result.getKind() !== SyntaxKind.StringLiteral) {
      throw PluginError.create(ErrorCode.INVALID_STRING_VALUE, '', variableName);
    }
    // Check constant value
    const resultValue = result.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
    if (!resultValue) {
      throw PluginError.create(ErrorCode.NOT_EMPTY_STRING, '', variableName);
    }
    return resultValue;
  }

  private parseCrossModuleVariable(scanDir: string): Map<string, string[]> {
    let sourceFiles = TsAstUtil.project!.addSourceFilesAtPaths(`${scanDir}/**/*.ets`);
    const exportMap = new Map<string, string[]>(); // key: path, value: exported variables
    for (let sourceFile of sourceFiles) {
      const exportedNames = this.getExportedVariables(sourceFile);
      if (exportedNames.length > 0) {
        exportMap.set(sourceFile.getFilePath(), exportedNames);
      }
    }
    return exportMap;
  }

  getExportedVariables(sourceFile: SourceFile): string[] {
    const exportSymbols: string[] = [];
    let exportKeywordNodes = sourceFile.getDescendantsOfKind(SyntaxKind.ExportKeyword);
    exportKeywordNodes.forEach((node) => {
      let parentNodeKind = node.getParent()?.getKind();
      switch (parentNodeKind) {
        case SyntaxKind.VariableStatement:
          let variableStatement = node.getParent()?.asKind(SyntaxKind.VariableStatement)!;
          let variableNames = variableStatement
            .getDeclarationList()
            .getDeclarations()
            .map((declaration) => {
              return declaration.getName();
            });
          exportSymbols.push(...variableNames);
          break;
        case SyntaxKind.ClassDeclaration:
          let classDeclaration = node.getParent();
          let className = classDeclaration?.asKind(SyntaxKind.ClassDeclaration)?.getName();
          exportSymbols.push(className!);
          break;
      }
    });
    return exportSymbols;
  }
}
